/*
 * Copyright 2016-2019 yoara
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package yhao.infra.apilist.util;

import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.*;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpMethod;
import yhao.infra.common.util.CommonMapUtil;

import javax.net.ssl.SSLContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * http请求工具类，提供常用的封装方式
 * <p>更复杂的用法，请自行实现，如连接池、复杂参数化配置等。</p>
 * @author yoara
 */
public class HttpClientUtil {
    private static Logger log = LoggerFactory.getLogger(HttpClientUtil.class);

    /**
     * Get或Post方法结果用于下载
     */
    public static void entityForDownload(byte[] content, HttpServletRequest request,
                                         HttpServletResponse response, String fileName,
                                         boolean closeOutputStream) throws IOException {
        InputStream stream = new ByteArrayInputStream(content);
        // Remove blank in the filename, but do not use regex(\s) for some reason
        fileName = fileName.replace(" ", "");

        // get client agent info，and encode the fileName.(only in IE、FF、Chrome)
        String agent = request.getHeader("USER-AGENT");
        if (agent != null && agent.toUpperCase().indexOf("MSIE") != -1) { // IE
            fileName = URLEncoder.encode(fileName, "UTF-8");
        } else {    // Other
            fileName = new String(fileName.getBytes(), "ISO-8859-1");
        }

        // output the data to client
        response.reset();
        response.addHeader("Content-Disposition", "attachment;filename=" + fileName);
        response.setContentType("application/octet-stream; charset=UTF-8");
        OutputStream out = null;
        try {
            out = response.getOutputStream();
            byte b[] = new byte[1024];
            int j;
            while ((j = stream.read(b)) != -1) {
                out.write(b, 0, j);
            }
            out.flush();
        } finally {
            if (closeOutputStream && out != null) {
                out.close();
            }
        }
    }

    public static HttpResult httpRequest(HttpParam param) {
        try{
            if (param.getMethod() == HttpMethod.POST) {
                HttpPost post = initPost(param);
                return execute(post, param);
            } else {
                HttpGet get = initGet(param);
                return execute(get, param);
            }
        }catch (Exception e){
            throw new RuntimeException(e.getMessage());
        }
    }

    public static HttpResult get(String uri) {
        return get(uri,null,false);
    }

    public static HttpResult get(String uri, String contentType) {
        return get(uri,contentType,false);
    }

    public static HttpResult get(String uri, String contentType, boolean isSSL) {
        HttpParam param = HttpParam.makeGetHttpParam(uri);
        if(StringUtils.isNotBlank(contentType)){
            param.setHeader(CommonMapUtil.NEW("Content-Type", contentType));
        }
        param.setNeedSsl(isSSL);
        return httpRequest(param);
    }

    public static HttpResult post(String uri) {
        return post(uri, null,false);
    }

    public static HttpResult post(String uri, String contentType) {
        return post(uri, contentType,false);
    }

    public static HttpResult post(String uri, String contentType, boolean isSSL) {
        HttpParam httpParam = HttpParam.makePostHttpParam(uri);
        if(StringUtils.isNotBlank(contentType)){
            httpParam.setHeader(CommonMapUtil.NEW("Content-Type", contentType));
        }
        httpParam.setNeedSsl(isSSL);
        return httpRequest(httpParam);
    }

    private static HttpGet initGet(HttpParam param) throws URISyntaxException {
        URIBuilder builder = initUriBuilder(param);
        HttpGet httpGet = new HttpGet(builder.build());
        if (param.getHeader() != null && param.getHeader().size() > 0) {
            for (Entry<String, String> entry : param.getHeader().entrySet()) {
                httpGet.addHeader(entry.getKey(), entry.getValue());
            }
        }
        httpGet.setProtocolVersion(param.getVersion());
        return httpGet;
    }

    private static URIBuilder initUriBuilder(HttpParam param) throws URISyntaxException {
        URIBuilder builder = new URIBuilder(param.uri);
        if(param.queryString!=null && param.queryString.size()>0){
            builder.addParameters(param.queryString);
        }
        return builder;
    }

    private static HttpPost initPost(HttpParam param) throws URISyntaxException, UnsupportedEncodingException {
        URIBuilder builder = initUriBuilder(param);
        HttpPost httpPost = new HttpPost(builder.build());
        if (param.getHeader() != null && param.getHeader().size() > 0) {
            for (Entry<String, String> entry : param.getHeader().entrySet()) {
                httpPost.addHeader(entry.getKey(), entry.getValue());
            }
        }
        StringEntity entity = null;
        if (param.getPostFormData() != null) {
            List<NameValuePair> nvps = new ArrayList<>();
            JSONObject paramJson = (JSONObject) JSONObject.toJSON(param.getPostFormData());
            for (Entry<String, Object> entry : paramJson.entrySet()) {
                Object value = entry.getValue();
                if (value == null) {
                    continue;
                }
                nvps.add(new BasicNameValuePair(entry.getKey(), value.toString()));
            }
            entity = new UrlEncodedFormEntity(nvps, Consts.UTF_8);
        }else if(param.getPostBodyString() != null){
            entity = new StringEntity(param.getPostBodyString(), Consts.UTF_8);
        }
        if(entity!=null && param.getContentType()!=null){
            entity.setContentType(param.getContentType().toString());
        }
        httpPost.setEntity(entity);
        httpPost.setProtocolVersion(param.getVersion());
        return httpPost;
    }

    private static HttpResult execute(HttpRequestBase request, HttpParam param) {
        CloseableHttpClient client = null;
        try {
            client = param.isNeedSsl() ? sslClient() : HttpClients.createDefault();
        } catch (Exception e) {
            log.error("HttpClientUtil ssl wrong:" + e.getMessage(), e);
            return null;
        }
        CloseableHttpResponse rsp = null;
        int status = 0;
        try {
            int timeOut = param.getTimeOut() > -1 ? param.getTimeOut() : 10000;
            RequestConfig requestConfig = RequestConfig.custom()
                    .setConnectTimeout(timeOut).setConnectionRequestTimeout(timeOut)
                    .setSocketTimeout(timeOut).build();
            request.setConfig(requestConfig);
            rsp = client.execute(request);
            status = rsp.getStatusLine().getStatusCode();
            HttpEntity entity = rsp.getEntity();
            ContentType contentType = ContentType.get(entity);
            HttpResult result = new HttpResult(EntityUtils.toByteArray(entity),status,contentType);
            Header contentDisposition = rsp.getFirstHeader("Content-disposition");
            if(contentDisposition!=null){
                result.contentDisposition = ContentDisposition.parse(contentDisposition.getValue());
            }
            return result;
        } catch (ClientProtocolException e) {
            log.error("HttpClientUtil found ClientProtocolException:" + e.getMessage(), e);
        } catch (IOException e) {
            log.error("HttpClientUtil found IOException:" + e.getMessage(), e);
        } finally {
            if(rsp!=null){
                try {
                    rsp.close();
                } catch (IOException e) {
                    log.error(e.getMessage());
                }
            }
            if (client != null) {
                try {
                    client.close();
                } catch (IOException e) {
                    log.error(e.getMessage());
                }
            }
        }
        return null;
    }

    private static CloseableHttpClient sslClient()
            throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException {
        // Trust own CA and all self-signed certs
        SSLContext sslcontext = SSLContexts.custom()
                .loadTrustMaterial((TrustStrategy) (chain, authType) -> true)
                //.loadTrustMaterial(new File("my.keystore"), "nopassword".toCharArray(),
                //        new TrustSelfSignedStrategy())
                .build();
        // Allow TLSv1 protocol only
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                sslcontext,
                new String[]{"TLSv1"},
                null,
                SSLConnectionSocketFactory.getDefaultHostnameVerifier());
        CloseableHttpClient httpclient = HttpClients.custom()
                .setSSLSocketFactory(sslsf)
                .build();
        return httpclient;
    }

    public static class HttpResult {
        private byte[] content;
        private int status;
        private ContentType contentType;
        private ContentDisposition contentDisposition;

        public HttpResult(byte[] content,int status,ContentType contentType) {
            this.content = content;
            this.status = status;
            this.contentType = contentType;
        }

        public byte[] getContent() {
            return content;
        }

        public int getStatus() {
            return status;
        }

        public ContentType getContentType() {
            return contentType;
        }

        public ContentDisposition getContentDisposition() {
            return contentDisposition;
        }

        public void contentForDownload(HttpServletRequest request,
                                       HttpServletResponse response, String fileName,
                                       boolean closeOutputStream) throws IOException {
            HttpClientUtil.entityForDownload(content,request,response,fileName,closeOutputStream);
        }

        public String contentToString(){
            Charset charset = null;
            try{
                charset = contentType.getCharset();
            }catch (Exception e){}
            if (charset == null) {
                charset = Charset.forName("UTF-8");
            }
            return new String(content,charset);
        }
    }

    public static class HttpParam {
        private HttpMethod method;
        private Map<String, String> header;
        private boolean needSsl;
        private String uri;
        private Object postFormData;
        private String postBodyString;
        private List<NameValuePair> queryString = new ArrayList<>();
        private int timeOut = -1;
        private HttpVersion version;
        private ContentType contentType;

        private HttpParam() {
        }

        public static HttpParam makePostHttpParam(String uri) {
            HttpParam param = new HttpParam();
            param.method = HttpMethod.POST;
            param.uri = uri;
            return param;
        }

        public static HttpParam makeGetHttpParam(String uri) {
            HttpParam param = new HttpParam();
            param.method = HttpMethod.GET;
            param.uri = uri;
            return param;
        }

        public void setHeader(Map<String, String> header) {
            this.header = header;
        }

        public void setNeedSsl(boolean needSsl) {
            this.needSsl = needSsl;
        }

        public Object getPostFormData() {
            return postFormData;
        }

        public void setPostFormData(Object postFormData) {
            this.postFormData = postFormData;
        }

        public HttpMethod getMethod() {
            return method;
        }

        public Map<String, String> getHeader() {
            return header;
        }

        public boolean isNeedSsl() {
            return needSsl;
        }


        public String getUri() {
            return uri;
        }

        public int getTimeOut() {
            return timeOut;
        }

        public void setTimeOut(int timeOut) {
            this.timeOut = timeOut;
        }

        public HttpVersion getVersion() {
            return version;
        }

        public void setVersion(HttpVersion version) {
            this.version = version;
        }

        public void addQueryString(String key,String value){
            queryString.add(new BasicNameValuePair(key,value));
        }

        public String getPostBodyString() {
            return postBodyString;
        }

        public void setPostBodyString(String postBodyString) {
            this.postBodyString = postBodyString;
        }

        public ContentType getContentType() {
            return contentType;
        }

        public HttpParam setContentType(ContentType contentType) {
            this.contentType = contentType;
            return this;
        }
    }
}
